package il.technion.cs236369.proxy;

import il.technion.cs236369.database.HttpMessageCacheDetails;
import il.technion.cs236369.database.ICache;
import il.technion.cs236369.proxy.communication.http.HttpProxyClient;
import java.io.IOException;
import java.net.Socket;
import java.util.Locale;
import javax.net.SocketFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.DefaultHttpClientConnection;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.util.EntityUtils;

/**
 * an HttpRequestHandler that handle a http request message to a proxy.
 * @author itamar talmi 0396055979 itamar.talmi@gmail.com , ofir haviv 300228921 havivi1986@gmail.com
 *
 */
public class ProxyHttpRequestHandler implements HttpRequestHandler {
	private long requestHitCount = 0;
	private long requestCount = 0;
	private HttpProxyClient proxyClient;
	private ICache cache;
	private SocketFactory sockFactory;
	private Socket clientConnection;
	private DefaultHttpClientConnection conn;

	private final int ClientPort = 80;
	private final String GetMethod = "GET";

	/**
	 * create a new ProxyHttpRequestHandler
	 * @param sockFactory - a socketFactory.
	 * @param cache - a cache to work with as proxy.
	 */
	public ProxyHttpRequestHandler(SocketFactory sockFactory, ICache cache) {
		this.proxyClient = new HttpProxyClient();
		this.cache = cache;
		this.sockFactory = sockFactory;
		clientConnection = null;
		conn = null;
	}

	@Override
	public void handle(final HttpRequest request, final HttpResponse response,
			final HttpContext context) throws HttpException, IOException {

		// System.out.println("HTTP SERVER GOT: " + request);
		requestCount++;
		String method = request.getRequestLine().getMethod()
				.toUpperCase(Locale.ENGLISH);

		if (!method.equals(GetMethod)) {
			throw new MethodNotSupportedException(method
					+ " method not supported");
		}

		HttpMessageCacheDetails cachedResponse = null;
		if (canUseCacheCheck(request))
			cachedResponse = TryGetFromCache(request);

		processRequest(request, response, cachedResponse);

		// System.out.println("HTTP SERVER RESPOND: " + response);
		/*if (requestCount%100==0){
			System.out.println();
			System.out.println("HITRATE update, after " + requestCount +" requests, hitrate is:" + getHitRate());
		}*/
	}

	/**
	 * precessing and dealing with a new http proxy request.
	 * @param request - the http request
	 * @param response - the final http response
	 * @param cachedResponse - the response as it is found in the cache or null if no cache entry found.
	 */
	private void processRequest(final HttpRequest request,
			final HttpResponse response, HttpMessageCacheDetails cachedResponse) {
		try {

			// Removing ACCEPT_ENCODING tag so the server won't give us zipped
			// content.
			request.removeHeaders(HttpHeaders.ACCEPT_ENCODING);
			
			request.removeHeaders(HttpHeaders.CONNECTION);
			addHeader(request, HttpHeaders.CONNECTION, "close");
			
			String host = GetHost(request);

			clientConnection = sockFactory.createSocket(host, ClientPort); 
			conn = new DefaultHttpClientConnection();

			if (cachedResponse != null) {
				addHeader(request, HttpHeaders.IF_NONE_MATCH,
						cachedResponse.etag);
		//		request.removeHeaders(HttpHeaders.CONNECTION);
		//		addHeader(request, HttpHeaders.CONNECTION, "close");
			}

			HttpResponse actualResponse = proxyClient.sendRequestToServer(
					clientConnection, conn, request);
			if (actualResponse == null) {
				response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
				return;
			}

			if (cachedResponse != null) {
				if (actualResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
					// Can use the cache.
					cachedResponse.WriteIntoHttpResponse(response);
					response.setStatusCode(HttpStatus.SC_OK);
					requestHitCount++;
					return;
				} else if(actualResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
					System.out.println("Here!!!");
					cache.deleteData(GetUrl(request));
					closeConnections();
					clientConnection = sockFactory.createSocket(host, ClientPort); 
					conn = new DefaultHttpClientConnection();
					request.removeHeaders(HttpHeaders.IF_NONE_MATCH);
					actualResponse = proxyClient.sendRequestToServer(
							clientConnection, conn, request);
					if (actualResponse == null) {
						response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
						return;
					}
				}
			}

			

			// Not in cache or need to refresh the page.
			CopyResponseIntoResponse(actualResponse, response);

			if (checkIfValidForCache(actualResponse))
				TrySaveIntoCache(GetUrl(request), response);

		} catch (Exception e) {

			response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);

			System.err.println("Error while tring to prepare the response to the client");
			
			//System.err.println("Error in ProxyHttpRequestHandler.processRequest");
			//System.err.println(e);
		} 
	}
	
	/**
	 * Returns the hitrate so far.
	 * @return the hitrate so far.
	 */
	public float getHitRate() {
		return ((float) requestHitCount) / ((float) requestCount);
	}
	
	private String GetUrl(HttpRequest req) {
		return req.getRequestLine().getUri();
	}

	private void CopyResponseIntoResponse(final HttpResponse source,
			final HttpResponse into) {
		// copy headers
		Header[] headers = source.getAllHeaders();
		into.setHeaders(headers);
		into.setEntity(source.getEntity());
		into.setStatusLine(source.getStatusLine());
	}

	private void addHeader(final HttpMessage httpMessage,
			final String headerName, final String headerValue) {
		if (httpMessage != null)
			httpMessage.setHeader(new BasicHeader(headerName, headerValue));
	}

	private boolean canUseCacheCheck(final HttpMessage httpMessage) {
		if (isHeaderValueExist(httpMessage, HttpHeaders.CACHE_CONTROL,
				new String[] { "no-store", "no-cache" })) {
			return false;
		}
		return true;
	}

	private boolean checkIfValidForCache(HttpResponse response) {
		if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK
				|| !canUseCacheCheck(response))
			return false;

		Header[] etagHeaders = response.getHeaders(HttpHeaders.ETAG);
		if (etagHeaders.length == 0) {
			// no ETag mechanism is the web site, can't cache.
			return false;
		}
		return true;
	}

	private boolean isHeaderValueExist(HttpMessage request, String headerName,
			String[] headerValues) {
		Header[] cacheControlHeaders = request.getHeaders(headerName);
		for (Header header : cacheControlHeaders) {
			String val = header.getValue();
			for (String headerValue : headerValues)
				if (val.compareTo(headerValue) == 0)
					return true;
		}
		return false;
	}

	private HttpMessageCacheDetails TryGetFromCache(HttpRequest request) {
		// return null;
		return cache.GetData(GetUrl(request));
	}

	final int constMaxBodyLengthForDB = 65535;

	private void TrySaveIntoCache(String url, HttpResponse response)
			throws IOException {
		HttpEntity entity = response.getEntity();
		if (url.length() > 255 || entity.isChunked() || entity.getContentLength() > constMaxBodyLengthForDB)
			return;

		
		byte[] body = EntityUtils.toByteArray(response.getEntity());
		response.setEntity(new ByteArrayEntity(body));

		if (body.length > constMaxBodyLengthForDB)
			return;
		cache.storeData(new HttpMessageCacheDetails(url, response
				.getAllHeaders(), body));
	}

	private String GetHost(final HttpMessage httpMessage) throws HttpException {
		Header[] hostHeaders = httpMessage.getHeaders(HttpHeaders.HOST);
		if (hostHeaders.length != 1)
			throw new HttpException("bad request - only one "
					+ HttpHeaders.HOST + "header is permited");
		String host = hostHeaders[0].getValue();
		return host;
	}

	/**
	 * Returns the request hit count so far.
	 * @return the request hit count so far.
	 */
	public long getRequestHitCount() {
		return requestHitCount;
	}

	/**
	 * Returns the request count so far.
	 * @return the request count so far.
	 */
	public long getRequestCount() {
		return requestCount;
	}
	
	/**
	 * closes the last connection established in handling the last proxy client request.
	 */
	public void closeConnections(){
	//	try { clientConnection.close(); } catch (Exception e) {}
		try { conn.close(); } catch (Exception e) {}
	}
}
